/*-------------------------------------------------------------------------
 *
 * toast_internals.c
 *	  用于 TOAST 系统内部使用的函数。
 *
 * Copyright (c) 2000-2022, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *	  src/backend/access/common/toast_internals.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/detoast.h"
#include "access/genam.h"
#include "access/heapam.h"
#include "access/heaptoast.h"
#include "access/table.h"
#include "access/toast_internals.h"
#include "access/xact.h"
#include "catalog/catalog.h"
#include "common/pg_lzcompress.h"
#include "miscadmin.h"
#include "utils/fmgroids.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"

static bool fc_toastrel_valueid_exists(Relation fc_toastrel, Oid fc_valueid);
static bool fc_toastid_valueid_exists(Oid fc_toastrelid, Oid fc_valueid);

/* ----------
 * toast_compress_datum -
 *
 *	创建一个 varlena 数据项的压缩版本
 *
 *	如果我们失败了（即，压缩结果实际上比原始数据项更大），
 *	则返回 NULL。我们不能使用压缩数据，如果它会扩大
 *	元组！
 *
 *	我们使用 VAR{SIZE,DATA}_ANY 以便可以在这里处理短的 varlena
 *	而不必复制它们。但我们不能处理外部或压缩数据项。
 * ----------
 */
Datum toast_compress_datum(Datum fc_value, char fc_cmethod)
{
	struct varlena *fc_tmp = NULL;
	int32		fc_valsize;
	ToastCompressionId fc_cmid = TOAST_INVALID_COMPRESSION_ID;

	Assert(!VARATT_IS_EXTERNAL(DatumGetPointer(fc_value)));
	Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(fc_value)));

	fc_valsize = VARSIZE_ANY_EXHDR(DatumGetPointer(fc_value));

	/* 如果压缩方法无效，则使用当前默认值 */
	if (!CompressionMethodIsValid(fc_cmethod))
		fc_cmethod = default_toast_compression;

	/*
	 * 对于压缩方法调用适当的压缩例程。
	 */
	switch (fc_cmethod)
	{
		case TOAST_PGLZ_COMPRESSION:
			fc_tmp = pglz_compress_datum((const struct varlena *) fc_value);
			fc_cmid = TOAST_PGLZ_COMPRESSION_ID;
			break;
		case TOAST_LZ4_COMPRESSION:
			fc_tmp = lz4_compress_datum((const struct varlena *) fc_value);
			fc_cmid = TOAST_LZ4_COMPRESSION_ID;
			break;
		default:
			elog(ERROR, "invalid compression method %c", fc_cmethod);
	}

	if (fc_tmp == NULL)
		return PointerGetDatum(NULL);

	/*
	 * 即使压缩报告成功，我们仍然重新检查实际大小，因为
	 * 它可能只保存了一字节的压缩数据 —— 一旦考虑到
	 * 头和对齐填充，这可能变成净损失。最坏的情况是，压缩格式可能
	 * 需要三个填充字节（加头，包含在 VARSIZE(tmp) 中），
	 * 而未压缩格式只需一个头字节，如果值足够短，则不需要填充。
	 * 所以我们坚持节省超过 2 字节以确保我们有收益。
	 */
	if (VARSIZE(fc_tmp) < fc_valsize - 2)
	{
		/* 成功的压缩 */
		Assert(fc_cmid != TOAST_INVALID_COMPRESSION_ID);
		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(fc_tmp, fc_valsize, fc_cmid);
		return PointerGetDatum(fc_tmp);
	}
	else
	{
		/* 无法压缩的数据 */
		pfree(fc_tmp);
		return PointerGetDatum(NULL);
	}
}

/* ----------
 * toast_save_datum -
 *
 *	将单个数据项保存到辅助关系中，并返回
 *	其 Datum 引用。
 *
 * rel: 我们正在处理的主要关系（不是 toast 关系！）
 * value: 要推送到 toast 存储的数据项
 * oldexternal: 如果不为 NULL，表示先前代表数据项的 toast 指针
 * options: 要传递给 heap_insert() 的 toast 行的选项
 * ----------
 */
Datum toast_save_datum(Relation fc_rel, Datum fc_value,
				 struct varlena *fc_oldexternal, int fc_options)
{
	Relation	fc_toastrel;
	Relation   *fc_toastidxs;
	HeapTuple	fc_toasttup;
	TupleDesc	fc_toasttupDesc;
	Datum		fc_t_values[3];
	bool		fc_t_isnull[3];
	CommandId	fc_mycid = GetCurrentCommandId(true);
	struct varlena *fc_result;
	struct varatt_external fc_toast_pointer;
	union
	{
		struct varlena hdr;
		/* 这是为了使联合体足够大以容纳一个块： */
		char		data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ];
		/* 确保联合体的对齐方式足够好： */
		int32		align_it;
	}			fc_chunk_data;
	int32		fc_chunk_size;
	int32		fc_chunk_seq = 0;
	char	   *fc_data_p;
	int32		fc_data_todo;
	Pointer		fc_dval = DatumGetPointer(fc_value);
	int			fc_num_indexes;
	int			fc_validIndex;

	Assert(!VARATT_IS_EXTERNAL(fc_value));

	/*
	 * 打开 toast 关系及其索引。我们可以使用索引来检查
	 * 分配给 toasted 项的 OID 的唯一性，尽管它有
	 * 除 OID 之外的附加列。
	 */
	fc_toastrel = table_open(fc_rel->rd_rel->reltoastrelid, RowExclusiveLock);
	fc_toasttupDesc = fc_toastrel->rd_att;

	/* 打开所有 toast 索引并查找有效索引 */
	fc_validIndex = toast_open_indexes(fc_toastrel,
									RowExclusiveLock,
									&fc_toastidxs,
									&fc_num_indexes);

	/*
	 * 获取数据指针和长度，并计算 va_rawsize 和 va_extinfo。
	 *
	 * va_rawsize 是等效全未压缩数据项的大小，因此
	 * 我们必须对短头进行调整。
	 *
	 * va_extinfo 存储 toast 记录中数据负载的实际大小，
	 * 如果数据被压缩，则在前两个位中存储压缩方法。
	 */
	if (VARATT_IS_SHORT(fc_dval))
	{
		fc_data_p = VARDATA_SHORT(fc_dval);
		fc_data_todo = VARSIZE_SHORT(fc_dval) - VARHDRSZ_SHORT;
		fc_toast_pointer.va_rawsize = fc_data_todo + VARHDRSZ;	/* 如同不短 */
		fc_toast_pointer.va_extinfo = fc_data_todo;
	}
	else if (VARATT_IS_COMPRESSED(fc_dval))
	{
		fc_data_p = VARDATA(fc_dval);
		fc_data_todo = VARSIZE(fc_dval) - VARHDRSZ;
		/* 压缩数据项中的原始大小仅为负载的大小 */
		fc_toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(fc_dval) + VARHDRSZ;

		/* 设置外部大小和压缩方法 */
		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(fc_toast_pointer, fc_data_todo,
													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(fc_dval));
		/* 断言数字看起来像是压缩的 */
		Assert(VARATT_EXTERNAL_IS_COMPRESSED(fc_toast_pointer));
	}
	else
	{
		fc_data_p = VARDATA(fc_dval);
		fc_data_todo = VARSIZE(fc_dval) - VARHDRSZ;
		fc_toast_pointer.va_rawsize = VARSIZE(fc_dval);
		fc_toast_pointer.va_extinfo = fc_data_todo;
	}

	/*
	 * 将正确的表 OID 插入结果 TOAST 指针中。
	 *
	 * 通常，这是目标 toast 表的实际 OID，但在
	 * 像 CLUSTER 这样的表重写操作期间，我们必须插入表的
	 * 真实永久 toast 表的 OID。如果我们必须替代这样的 OID，
	 * rd_toastoid 会被设置。
	 */
	if (OidIsValid(fc_rel->rd_toastoid))
		fc_toast_pointer.va_toastrelid = fc_rel->rd_toastoid;
	else
		fc_toast_pointer.va_toastrelid = RelationGetRelid(fc_toastrel);

	/*
	 * 选择一个 OID 作为此 toast 值的值 ID。
	 *
	 * 通常我们只是选择一个未使用的 OID 在 toast 表中。但
	 * 在表重写操作期间，我们保留现有的
	 * toast 表 OID 时，我们也想保留 toast 值 OIDs。因此，如果
	 * rd_toastoid 被设置且我们有一个来自同一
	 * toast 表的先前外部值，则重用其值 ID。如果我们没有先前的外部
	 * 值（这是一个边缘案例，但如果表的 attstorage
	 * 选项被更改，则可能发生），我们必须选择一个值 ID，以免与新的或现有的 toast 值 OID 冲突。
	 */
	if (!OidIsValid(fc_rel->rd_toastoid))
	{
		/* 正常情况：只需选择一个未使用的 OID */
		fc_toast_pointer.va_valueid =
			GetNewOidWithIndex(fc_toastrel,
							   RelationGetRelid(fc_toastidxs[fc_validIndex]),
							   (AttrNumber) 1);
	}
	else
	{
		/* 重写情况：检查值是否在旧 toast 表中 */
		fc_toast_pointer.va_valueid = InvalidOid;
		if (fc_oldexternal != NULL)
		{
			struct varatt_external fc_old_toast_pointer;

			Assert(VARATT_IS_EXTERNAL_ONDISK(fc_oldexternal));
			/* 必须复制以访问对齐字段 */
			VARATT_EXTERNAL_GET_POINTER(fc_old_toast_pointer, fc_oldexternal);
			if (fc_old_toast_pointer.va_toastrelid == fc_rel->rd_toastoid)
			{
				/* 此值来自旧的 toast 表；重用其 OID */
				fc_toast_pointer.va_valueid = fc_old_toast_pointer.va_valueid;

				/*
				 * 这里有一个边缘案例：表重写可能需要
				 * 复制一行的活动版本和最近删除版本，
				 * 这些版本可能很容易引用相同的 toast 值。
				 * 当我们复制第二个或后续版本的此类行时，
				 * 重用 OID 意味着我们选择一个已经在
				 * 新 toast 表中的 OID。检查这一点，如果是，
				 * 则直接跳过，而不再写入数据。
				 *
				 * 虽然烦人且外观丑陋，但这是件好事
				 * 因为它确保当旧
				 * toast 表中只有一个副本时，我们最后只有一个 toast 值的副本。在我们检测到这种情况之前，我们会制作
				 * 多个副本，浪费空间；更糟糕的是，
				 * 属于已删除堆元组的副本将不会被 VACUUM 回收。
				 */
				if (fc_toastrel_valueid_exists(fc_toastrel,
											fc_toast_pointer.va_valueid))
				{
					/* 匹配，因此短路下面的数据存储循环 */
					fc_data_todo = 0;
				}
			}
		}
		if (fc_toast_pointer.va_valueid == InvalidOid)
		{
			/*
			 * 新值；必须选择一个在旧 toast 表或新 toast 表中都不冲突的 OID
			 */
			do
			{
				fc_toast_pointer.va_valueid =
					GetNewOidWithIndex(fc_toastrel,
									   RelationGetRelid(fc_toastidxs[fc_validIndex]),
									   (AttrNumber) 1);
			} while (fc_toastid_valueid_exists(fc_rel->rd_toastoid,
											fc_toast_pointer.va_valueid));
		}
	}

	/*
	 * 初始化元组数据的常量部分
	 */
	fc_t_values[0] = ObjectIdGetDatum(fc_toast_pointer.va_valueid);
	fc_t_values[2] = PointerGetDatum(&fc_chunk_data);
	fc_t_isnull[0] = false;
	fc_t_isnull[1] = false;
	fc_t_isnull[2] = false;

	/*
	 * 将项拆分为块
	 */
	while (fc_data_todo > 0)
	{
		int			fc_i;

		CHECK_FOR_INTERRUPTS();

		/*
		 * 计算该块的大小
		 */
		fc_chunk_size = Min(TOAST_MAX_CHUNK_SIZE, fc_data_todo);

		/*
		 * 构建一个元组并存储它
		 */
		fc_t_values[1] = Int32GetDatum(fc_chunk_seq++);
		SET_VARSIZE(&fc_chunk_data, fc_chunk_size + VARHDRSZ);
		memcpy(VARDATA(&fc_chunk_data), fc_data_p, fc_chunk_size);
		fc_toasttup = heap_form_tuple(fc_toasttupDesc, fc_t_values, fc_t_isnull);

		heap_insert(fc_toastrel, fc_toasttup, fc_mycid, fc_options, NULL);

		/*
		 * 创建索引条目。我们在这里稍微作弊，不使用
		 * FormIndexDatum：这依赖于索引列
		 * 与所有索引的表的初始列相同的知识。我们也稍微作弊，
		 * 不提供 IndexInfo：目前这样没关系，因为 btree 不需要，但将来我们可能需要更诚实。
		 *
		 * 另请注意，TOAST 表上最好没有任何用户创建的索引，
		 * 因为我们不麻烦更新其他任何东西。
		 */
		for (fc_i = 0; fc_i < fc_num_indexes; fc_i++)
		{
			/* 只有标记为准备好的索引关系才能被更新 */
			if (fc_toastidxs[fc_i]->rd_index->indisready)
				index_insert(fc_toastidxs[fc_i], fc_t_values, fc_t_isnull,
							 &(fc_toasttup->t_self),
							 fc_toastrel,
							 fc_toastidxs[fc_i]->rd_index->indisunique ?
							 UNIQUE_CHECK_YES : UNIQUE_CHECK_NO,
							 false, NULL);
		}

		/*
		 * 释放内存
		 */
		heap_freetuple(fc_toasttup);

		/*
		 * 继续下一个块
		 */
		fc_data_todo -= fc_chunk_size;
		fc_data_p += fc_chunk_size;
	}

	/*
	 * 完成 - 关闭 toast 关系及其索引，但在提交之前保持锁定，以便在 toast 关系上直接进行的并发重建索引能够等待此事务。
	 */
	toast_close_indexes(fc_toastidxs, fc_num_indexes, NoLock);
	table_close(fc_toastrel, NoLock);

	/*
	 * 创建我们将返回的 TOAST 指针值
	 */
	fc_result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
	SET_VARTAG_EXTERNAL(fc_result, VARTAG_ONDISK);
	memcpy(VARDATA_EXTERNAL(fc_result), &fc_toast_pointer, sizeof(fc_toast_pointer));

	return PointerGetDatum(fc_result);
}

/* ----------
 * toast_delete_datum -
 *
 *	删除单个外部存储值。
 * ----------
 */
void toast_delete_datum(Relation fc_rel, Datum fc_value, bool fc_is_speculative)
{
	struct varlena *fc_attr = (struct varlena *) DatumGetPointer(fc_value);
	struct varatt_external fc_toast_pointer;
	Relation	fc_toastrel;
	Relation   *fc_toastidxs;
	ScanKeyData fc_toastkey;
	SysScanDesc fc_toastscan;
	HeapTuple	fc_toasttup;
	int			fc_num_indexes;
	int			fc_validIndex;
	SnapshotData fc_SnapshotToast;

	if (!VARATT_IS_EXTERNAL_ONDISK(fc_attr))
		return;

	/* 必须复制以访问对齐字段 */
	VARATT_EXTERNAL_GET_POINTER(fc_toast_pointer, fc_attr);

	/*
	 * 打开toast关系及其索引
	 */
	fc_toastrel = table_open(fc_toast_pointer.va_toastrelid, RowExclusiveLock);

	/* 获取用于处理的有效关系 */
	fc_validIndex = toast_open_indexes(fc_toastrel,
									RowExclusiveLock,
									&fc_toastidxs,
									&fc_num_indexes);

	/*
	 * 设置扫描键以查找与 va_valueid 匹配的块
	 */
	ScanKeyInit(&fc_toastkey,
				(AttrNumber) 1,
				BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(fc_toast_pointer.va_valueid));

	/*
	 * 找到所有块。（我们实际上不关心是否按顺序看到它们，但由于我们已经锁定了索引，不妨使用 systable_beginscan_ordered。）
	 */
	init_toast_snapshot(&fc_SnapshotToast);
	fc_toastscan = systable_beginscan_ordered(fc_toastrel, fc_toastidxs[fc_validIndex],
										   &fc_SnapshotToast, 1, &fc_toastkey);
	while ((fc_toasttup = systable_getnext_ordered(fc_toastscan, ForwardScanDirection)) != NULL)
	{
		/*
		 * 有一个块，删除它
		 */
		if (fc_is_speculative)
			heap_abort_speculative(fc_toastrel, &fc_toasttup->t_self);
		else
			simple_heap_delete(fc_toastrel, &fc_toasttup->t_self);
	}

	/*
	 * 结束扫描并关闭关系，但在提交之前保持锁定，以便在 toast 关系上直接进行的并发重建索引能够等待此事务。
	 */
	systable_endscan_ordered(fc_toastscan);
	toast_close_indexes(fc_toastidxs, fc_num_indexes, NoLock);
	table_close(fc_toastrel, NoLock);
}

/* ----------
 * toastrel_valueid_exists -
 *
 *	测试给定 ID 的 toast 值是否存在于 toast 关系中。
 *	为安全起见，我们认为，如果该 ID 有任何活动或已死的 toast 行，则该值存在；有关详细信息，请参见 GetNewOidWithIndex() 的注释。
 * ----------
 */
static bool fc_toastrel_valueid_exists(Relation fc_toastrel, Oid fc_valueid)
{
	bool		fc_result = false;
	ScanKeyData fc_toastkey;
	SysScanDesc fc_toastscan;
	int			fc_num_indexes;
	int			fc_validIndex;
	Relation   *fc_toastidxs;

	/* 获取有效的索引关系 */
	fc_validIndex = toast_open_indexes(fc_toastrel,
									RowExclusiveLock,
									&fc_toastidxs,
									&fc_num_indexes);

	/*
	 * 设置扫描键以查找与 va_valueid 匹配的块
	 */
	ScanKeyInit(&fc_toastkey,
				(AttrNumber) 1,
				BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(fc_valueid));

	/*
	 * 是否有这样的块？
	 */
	fc_toastscan = systable_beginscan(fc_toastrel,
								   RelationGetRelid(fc_toastidxs[fc_validIndex]),
								   true, SnapshotAny, 1, &fc_toastkey);

	if (systable_getnext(fc_toastscan) != NULL)
		fc_result = true;

	systable_endscan(fc_toastscan);

	/* 清理 */
	toast_close_indexes(fc_toastidxs, fc_num_indexes, RowExclusiveLock);

	return fc_result;
}

/* ----------
 * toastid_valueid_exists -
 *
 *	如上，但使用 toast 关系的 OID 而不是打开的关系
 * ----------
 */
static bool fc_toastid_valueid_exists(Oid fc_toastrelid, Oid fc_valueid)
{
	bool		fc_result;
	Relation	fc_toastrel;

	fc_toastrel = table_open(fc_toastrelid, AccessShareLock);

	fc_result = fc_toastrel_valueid_exists(fc_toastrel, fc_valueid);

	table_close(fc_toastrel, AccessShareLock);

	return fc_result;
}

/* ----------
 * toast_get_valid_index
 *
 *	获取与给定 toast 关系关联的有效索引的 OID。一个 toast
 *	关系一次只能有一个有效索引。
 */
Oid toast_get_valid_index(Oid fc_toastoid, LOCKMODE fc_lock)
{
	int			fc_num_indexes;
	int			fc_validIndex;
	Oid			fc_validIndexOid;
	Relation   *fc_toastidxs;
	Relation	fc_toastrel;

	/* 打开toast关系 */
	fc_toastrel = table_open(fc_toastoid, fc_lock);

	/* 查找 toast 关系的有效索引 */
	fc_validIndex = toast_open_indexes(fc_toastrel,
									fc_lock,
									&fc_toastidxs,
									&fc_num_indexes);
	fc_validIndexOid = RelationGetRelid(fc_toastidxs[fc_validIndex]);

	/* 关闭 toast 关系及其所有索引 */
	toast_close_indexes(fc_toastidxs, fc_num_indexes, NoLock);
	table_close(fc_toastrel, NoLock);

	return fc_validIndexOid;
}

/* ----------
 * toast_open_indexes
 *
 *	获取与给定 toast 关系关联的索引数组，并返回该数组中 toast 关系使用的有效索引的位置。调用此
 *	函数的责任在于关闭这些索引并释放它们。
 */
int toast_open_indexes(Relation fc_toastrel,
				   LOCKMODE fc_lock,
				   Relation **fc_toastidxs,
				   int *fc_num_indexes)
{
	int			fc_i = 0;
	int			fc_res = 0;
	bool		fc_found = false;
	List	   *fc_indexlist;
	ListCell   *fc_lc;

	/* 获取 toast 关系的索引列表 */
	fc_indexlist = RelationGetIndexList(fc_toastrel);
	Assert(fc_indexlist != NIL);

	*fc_num_indexes = list_length(fc_indexlist);

	/* 打开所有索引关系 */
	*fc_toastidxs = (Relation *) palloc(*fc_num_indexes * sizeof(Relation));
	foreach(fc_lc, fc_indexlist)
		(*fc_toastidxs)[fc_i++] = index_open(lfirst_oid(fc_lc), fc_lock);

	/* 获取列表中的第一个有效索引 */
	for (fc_i = 0; fc_i < *fc_num_indexes; fc_i++)
	{
		Relation	fc_toastidx = (*fc_toastidxs)[fc_i];

		if (fc_toastidx->rd_index->indisvalid)
		{
			fc_res = fc_i;
			fc_found = true;
			break;
		}
	}

	/*
	 * 释放索引列表，因为关系已打开且已找到有效索引，因此不再需要。
	 */
	list_free(fc_indexlist);

	/*
	 * toast 关系应该有一个有效索引，如果没有，则说明出现了问题。
	 */
	if (!fc_found)
		elog(ERROR, "no valid index found for toast relation with Oid %u",
			 RelationGetRelid(fc_toastrel));

	return fc_res;
}

/* ----------
 * toast_close_indexes
 *
 *	关闭 toast 关系的索引数组并释放它。这应该在之前使用 toast_open_indexes 打开的索引集合上调用。
 */
void toast_close_indexes(Relation *fc_toastidxs, int fc_num_indexes, LOCKMODE fc_lock)
{
	int			fc_i;

	/* 关闭关系并清理 */
	for (fc_i = 0; fc_i < fc_num_indexes; fc_i++)
		index_close(fc_toastidxs[fc_i], fc_lock);
	pfree(fc_toastidxs);
}

/* ----------
 * init_toast_snapshot
 *
 *	初始化适当的 TOAST 快照。我们必须使用一个 MVCC 快照
 *	来初始化 TOAST 快照；由于我们不知道使用哪个，
 *	只需使用最旧的一个即可。这是安全的：在最坏的情况下，我们将得到一个“快照过旧”的错误，这本可以避免。
 */
void init_toast_snapshot(Snapshot fc_toast_snapshot)
{
	Snapshot	fc_snapshot = GetOldestSnapshot();

	/*
	 * GetOldestSnapshot 如果会话没有活动快照，则返回 NULL。
	 * 比如说，一个过程将一个 toasted 值获取到一个局部变量中，
	 * 提交，然后尝试解除 toast。这样的编码是不安全的，
	 * 因为一旦我们提交，就没有什么可以阻止 toast 数据被删除。
	 * 解除 toast *必须* 在最初获取 toast 指针的同一事务中进行。
	 * 因此，与其尝试解决这个问题，不如抛出一个错误。（这并不是很大的保护，
	 * 因为在许多场景中，过程可能已经创建了一个新的事务快照，
	 * 阻止我们检测到问题。但这总比没有要好，肯定的，我们不应该消耗代码来掩盖问题。）
	 */
	if (fc_snapshot == NULL)
		elog(ERROR, "cannot fetch toast data without an active snapshot");

	/*
	 * Catalog snapshots 可以由 GetOldestSnapshot() 返回，即使它们没有
	 * 注册或活动。这很容易隐藏有关没有设置快照的错误——大多数情况下会有一个有效的目录快照。
	 * 所以还要坚持当前快照是注册或活动的。
	 */
	Assert(HaveRegisteredOrActiveSnapshot());

	InitToastSnapshot(*fc_toast_snapshot, fc_snapshot->lsn, fc_snapshot->whenTaken);
}
